-- 
-- Please see the readme.txt file included with this distribution for 
-- attribution and copyright information.
--

SPECIAL_MSGTYPE_WHISPER = "whisper";
SPECIAL_MSGTYPE_DICETOWER = "dicetower";
SPECIAL_MSGTYPE_APPLYDMG = "applydmg";
SPECIAL_MSGTYPE_APPLYEFF = "applyeff";
SPECIAL_MSGTYPE_EXPIREEFF = "expireeff";
SPECIAL_MSGTYPE_FAILEDSAVEEFF = "failedsaveeff";
SPECIAL_MSGTYPE_ENDTURN = "endturn";
SPECIAL_MSGTYPE_REMOVECLIENTTARGET = "removeclienttarget";

-- Initialization
function onInit()
	registerSlashHandler("/whisper", processWhisper);
	registerSlashHandler("/w", processWhisper);
	registerSlashHandler("/wg", processWhisperGM);
	registerSlashHandler("/die", processDie);
	registerSlashHandler("/mod", processMod);
	
	registerSpecialMsgHandler(SPECIAL_MSGTYPE_WHISPER, handleWhisper);
	registerSpecialMsgHandler(SPECIAL_MSGTYPE_DICETOWER, handleDiceTower);
	registerSpecialMsgHandler(SPECIAL_MSGTYPE_APPLYDMG, handleApplyDamage);
	registerSpecialMsgHandler(SPECIAL_MSGTYPE_APPLYEFF, handleApplyEffect);
	registerSpecialMsgHandler(SPECIAL_MSGTYPE_EXPIREEFF, handleExpireEffect);
	registerSpecialMsgHandler(SPECIAL_MSGTYPE_FAILEDSAVEEFF, handleFailedSaveEffect);
	registerSpecialMsgHandler(SPECIAL_MSGTYPE_ENDTURN, handleEndTurn);
	registerSpecialMsgHandler(SPECIAL_MSGTYPE_REMOVECLIENTTARGET, handleRemoveClientTarget);
end

-- Chat window registration for general purpose message dispatching
function registerControl(ctrl)
	control = ctrl;
end

function registerEntryControl(ctrl)
	entrycontrol = ctrl;
	ctrl.onSlashCommand = onSlashCommand;
end

function registerWindowControl(ctrl)
	windowcontrol = ctrl;
end

function DieControlThrow(type, bonus, name, custom, dice)
	if control then
		control.throwDice(type, dice, bonus, name, custom);
	end
end

-- Generic message delivery
function deliverMessage(msg, recipients)
	if control then
		control.deliverMessage(msg, recipients);
	end
end

function addMessage(msg)
	if control then
		control.addMessage(msg);
	end
end

launchmsg = {};

function registerLaunchMessage(msg)
	table.insert(launchmsg, msg);
end

function retrieveLaunchMessages()
	return launchmsg;
end

--
--
-- INTERNAL FUNCTIONS
--
--

function debug(...)
	local aOutput = {};
	for i = 1, select("#", ...) do
		table.insert(aOutput, convertVariableToString(select(i, ...)));
	end
	
	Message(table.concat(aOutput, " | "));
end

function debug_log(...)
	local aOutput = {};
	for i = 1, select("#", ...) do
		table.insert(aOutput, convertVariableToString(select(i, ...)));
	end
	
	print(table.concat(aOutput, " | "));
end

function convertVariableToString(variable, done)
	-- HANDLE NIL
	if variable == nil then
		return "nil";
	end

	-- SETUP
	local sType = type(variable);
	local sOutput = "";
	
	-- HANDLE BASIC VALUES
	if sType == "number" then
		sOutput = "#" .. variable;

	elseif sType == "string" then
		sOutput = "s'" .. variable .. "'";

	elseif sType == "boolean" then
		if variable then
			sOutput = sOutput .. "bTRUE";
		else
			sOutput = sOutput .. "bFALSE";
		end

	-- HANDLE TABLES
	elseif sType == "table" then
		local tableResults = {};
		for k, v in pairs(variable) do
			local sTemp = tostring(k) .. " = " .. convertVariableToString(v);
			table.insert(tableResults, sTemp);
		end
		sOutput = "{ " .. table.concat(tableResults, ", ") .. " }";

	-- HANDLE CUSTOM OR UNKNOWN TYPES
	else
		sOutput = string.upper(sType) .. " = ";

		if sType == "dragdata" then
			local tableResults = {};
			table.insert(tableResults, "#slots = " .. convertVariableToString(variable.getSlotCount()));
			table.insert(tableResults, "desc = " .. convertVariableToString(variable.getDescription()));
			table.insert(tableResults, "dice = " .. convertVariableToString(variable.getDieList()));
			table.insert(tableResults, "num = " .. convertVariableToString(variable.getNumberData()));
			table.insert(tableResults, "shortcut = " .. convertVariableToString(variable.getShortcutData()));
			table.insert(tableResults, "slot = " .. convertVariableToString(variable.getSlot()));
			table.insert(tableResults, "string = " .. convertVariableToString(variable.getStringData()));
			table.insert(tableResults, "token = " .. convertVariableToString(variable.getTokenData()));
			table.insert(tableResults, "custom = " .. convertVariableToString(variable.getCustomData()));
			
			sOutput = sOutput .. "{ " .. table.concat(tableResults, ",") .. " }";

		elseif sType == "databasenode" then
			sOutput = sOutput .. variable.getNodeName();

		elseif sType == "windowinstance" then
			sOutput = sOutput .. "{ class = " .. variable.getClass() .. ", node = ";
			if variable.getDatabaseNode() then
				sOutput = sOutput .. variable.getDatabaseNode().getNodeName();
			else
				sOutput = sOutput .. "nil";
			end
			sOutput = sOutput .. " }";

		elseif sType == "numbercontrol" or sType == "stringcontrol" then
			sOutput = sOutput .. variable.getValue();

		elseif sType == "bitmapwidget" then
			sOutput = sOutput .. variable.getBitmapName();
		
		else
			sOutput = sOutput .. tostring(t);
		end
	end

	-- RESULTS
	return sOutput;
end


--
--
-- ROLL HANDLER
--
--

rollhandlers = {};

function registerRollHandler(sRollType, callback)
	rollhandlers[sRollType] = callback;
end

function unregisterRollHandler(sRollType, callback)
	rollhandlers[sRollType] = nil;
end

function onDiceRoll(draginfo)
	local sRollType = draginfo.getType();
	
	for k, v in pairs(rollhandlers) do
		if k == sRollType then
			v(draginfo);
			return true;
		end
	end
	
	if rollhandlers[""] then
		rollhandlers[""](draginfo);
		return true;
	end
	
	return nil;
end


--
--
-- SLASH COMMAND HANDLER
--
--

slashhandlers = {};

function registerSlashHandler(command, callback)
	slashhandlers[command] = callback;
end

function unregisterSlashHandler(command, callback)
	slashhandlers[command] = nil;
end

function onSlashCommand(command, parameters)
	for c, h in pairs(slashhandlers) do
		if string.find(string.lower(c), string.lower(command), 1, true) == 1 then
			h(parameters);
			return;
		end
	end
end


--
--
-- AUTO-COMPLETE
--
--

function doAutocomplete()
	local buffer = entrycontrol.getValue();
	local spacepos = string.find(string.reverse(buffer), " ", 1, true);
	
	local search = "";
	local remainder = buffer;
	
	if spacepos then
		search = string.sub(buffer, #buffer - spacepos + 2);
		remainder = string.sub(buffer, 1, #buffer - spacepos + 1);
	else
		search = buffer;
		remainder = "";
	end
	
	-- Check identities
	for k, v in ipairs(User.getAllActiveIdentities()) do
		local label = User.getIdentityLabel(v);
		if label and string.find(string.lower(label), string.lower(search), 1, true) == 1 then
			local replacement = remainder .. label .. " ";
			entrycontrol.setValue(replacement);
			entrycontrol.setCursorPosition(#replacement + 1);
			entrycontrol.setSelectionPosition(#replacement + 1);
			return;
		end
	end
end


--
--
-- DICE AND MOD SLASH HANDLERS
--
--

function processDie(params)
	if control then
		if User.isHost() then
			if params == "reveal" then
				OptionsManager.setOption("REVL", "on");
				SystemMessage("Revealing all die rolls");
				return;
			end
			if params == "hide" then
				OptionsManager.setOption("REVL", "off");
				SystemMessage("Hiding all die rolls");
				return;
			end
		end
	
		local diestring, descriptionstring = string.match(params, "%s*(%S+)%s*(.*)");
		
		if not diestring then
			SystemMessage("Usage: /die [dice] [description]");
			return;
		end
		
		local dice, modifier = StringManager.convertStringToDice(diestring);
		
		RulesManager.dclkAction("dice", modifier, descriptionstring, nil, nil, dice, true);
	end
end

function processMod(params)
	if control then
		local modstring, descriptionstring = string.match(params, "%s*(%S+)%s*(.*)");
		
		local modifier = tonumber(modstring);
		if not modifier then
			SystemMessage("Usage: /mod [number] [description]");
			return;
		end
		
		ModifierStack.addSlot(descriptionstring, modifier);
	end
end


--
--
-- MESSAGES
--
--

function createBaseMessage(rSourceActor)
	-- Set up the basic message components
	local msg = {font = "systemfont", text = "", dicesecret = false};

	-- GET SOURCE ACTOR NAME
	local sName = "";
	if rSourceActor then
		local sOptionShowRoll = OptionsManager.getOption("SHRL");
		if (sOptionShowRoll == "all") or ((sOptionShowRoll == "pc") and (rSourceActor.sType == "pc")) then
			sName = rSourceActor.sName;
		end
	end
	
	-- ADD THE SOURCE ACTOR NAME TO THE OUTPUT TEXT IF AVAILABLE, 
	-- OTHERWISE SET THE ACTIVE IDENTITY AS THE SPEAKER (USUALLY A NON-ACTION)
	if sName ~= "" then
		msg.text = sName .. " -> " .. msg.text;
	else
		if User.isHost() then
			msg.sender = GmIdentityManager.getCurrent();
		else
			msg.sender = User.getIdentityLabel();
		end
	end
	
	-- PORTRAIT CHAT?
	if OptionsManager.isOption("PCHT", "on") then
		if User.isHost() then
			msg.icon = "portrait_gm_token";
		else
			msg.icon = "portrait_" .. User.getCurrentIdentity() .. "_chat";
		end
	end

	-- RESULTS
	return msg;
end

-- Message: prints a message in the Chatwindow
function Message(msgtxt, broadcast, rActor)
	local msg = createBaseMessage(rActor);
	msg.text = msg.text .. msgtxt;
	if broadcast then
		deliverMessage(msg);
	else
		addMessage(msg);
	end
end

-- SystemMessage: prints a message in the Chatwindow
function SystemMessage(msgtxt)
	local msg = {font = "systemfont"};
	msg.text = msgtxt;
	addMessage(msg);
end


--
--
-- CHAT REPORTS
--
--

function reportEffect(rEffect, sTarget)
	-- VALIDATE
	if not rEffect or not rEffect.sName or not rEffect.sExpire then
		return;
	end
	
	-- Build the basic message
	local msg = {font = "systemfont", icon = "indicator_effect"};
	
	-- Add effect details
	msg.text = RulesManager.encodeEffectAsText(rEffect, sTarget);
	
	-- Add the save modifier as a number to make the chat entry draggable
	msg.dicesecret = true;
	msg.dice = {};
	if rEffect.sExpire == "save" then
		msg.diemodifier = rEffect.nSaveMod;
	end
	
	-- Add the message locally, and deliver to host, if needed
	if User.isHost() then
		addMessage(msg);
	else
		deliverMessage(msg);
	end
end

function reportModifier(mod_name, mod_bonus)
	-- Build the basic modifier message
	local msg = {font = "systemfont"};
	msg.text = mod_name;

	-- Add the save modifier as a number to make the chat entry draggable
	msg.dicesecret = true;
	msg.dice = {};
	msg.diemodifier = mod_bonus;

	-- Deliver the message
	if User.isHost() then
		addMessage(msg);
	else
		deliverMessage(msg);
	end
end

function messageAttack(bGMOnly, sTotal, rTargetActor, sExtraResult)
	if not (rTargetActor or sExtraResult ~= "") then
		return;
	end
	
	local msg = {font = "msgfont", icon = "indicator_sword"};
	msg.text = "Attack [" .. sTotal .. "] ->";
	if rTargetActor then
		msg.text = msg.text .. " [at " .. rTargetActor.sName .. "]";
	end
	if sExtraResult and sExtraResult ~= "" then
		msg.text = msg.text .. " " .. sExtraResult;
	end
	if bGMOnly then
		msg.text = "[GM] " .. msg.text;
		addMessage(msg);
	else
		deliverMessage(msg);
	end
end

function messageDamage(bGMOnly, sDamageType, sTotal, rTargetActor, sExtraResult)
	if not (rTargetActor or sExtraResult ~= "") then
		return;
	end
	
	local msg = {font = "msgfont"};
	if sDamageType == "Heal" or sDamageType == "Temporary hit points" then
		msg.icon = "indicator_heal";
	else
		msg.icon = "indicator_damage";
	end
	msg.text = sDamageType .. " [" .. sTotal .. "] ->";
	if rTargetActor then
		msg.text = msg.text .. " [to " .. rTargetActor.sName .. "]";
	end
	if sExtraResult and sExtraResult ~= "" then
		msg.text = msg.text .. " " .. sExtraResult;
	end
	if bGMOnly then
		msg.text = "[GM] " .. msg.text;
		addMessage(msg);
	else
		deliverMessage(msg);
	end
end

--
--
-- SPECIAL MESSAGE HANDLING
--
--

SPECIAL_MSG_TAG = "[SPECIAL]";
SPECIAL_MSG_SEP = "|||";

specialmsghandlers = {};

function registerSpecialMsgHandler(msgtype, callback)
	specialmsghandlers[msgtype] = callback;
end

function sendSpecialMessage(msgtype, params)
	-- Hosts go directly to handling the message
	if User.isHost() then
		handleSpecialMessage(msgtype, "", "", params);
	
	-- Clients build a special message to send to the host
	else
		-- Build the special message to send
		local msg = {font = "msgfont", text = ""};
		msg.sender = SPECIAL_MSG_TAG .. SPECIAL_MSG_SEP .. msgtype .. SPECIAL_MSG_SEP .. User.getUsername() .. SPECIAL_MSG_SEP .. User.getIdentityLabel() .. SPECIAL_MSG_SEP;
		for k,v in pairs(params) do
			msg.text = msg.text .. v .. SPECIAL_MSG_SEP;
		end

		-- Deliver to the host
		deliverMessage(msg, "");
	end
end

function processSpecialMessage(msg)
	-- Only the host can process special messages
	if not User.isHost() then
		return false;
	end
	
	-- Make sure the sender this is a special message
	if string.find(msg.sender, SPECIAL_MSG_TAG, 1, true) ~= 1 then
		return false;
	end

	-- Parse out the special message details
	local msg_meta = {};
	local msg_clause;
	local clause_match = "(.-)" .. SPECIAL_MSG_SEP;
	for msg_clause in string.gmatch(msg.sender, clause_match) do
		table.insert(msg_meta, msg_clause);
	end
	local msgtype = msg_meta[2];
	local msguser = msg_meta[3];
	local msgidentity = msg_meta[4];
	
	-- Parse out the special message parameters
	local msg_params = {};
	for msg_clause in string.gmatch(msg.text, clause_match) do
		table.insert(msg_params, msg_clause);
	end
	
	-- Handle the special message
	handleSpecialMessage(msgtype, msguser, msgidentity, msg_params);
	return true;
end

function handleSpecialMessage(msgtype, msguser, msgidentity, paramlist)
	for k,v in pairs(specialmsghandlers) do
		if msgtype == k then
			v(msguser, msgidentity, paramlist);
			return;
		end
	end
	
	SystemMessage("[ERROR] Unknown special message received, Type = " .. msgtype);
end


--
--
-- WHISPERS
--
--

function processWhisper(params)
	-- Find the target user for the whisper, if available
	local lower_msg = string.lower(params);
	local user = nil;
	local recipient = nil;
	for k, v in ipairs(User.getAllActiveIdentities()) do
		local label = User.getIdentityLabel(v);
		local label_match = string.lower(label) .. " ";
		if string.sub(lower_msg, 1, string.len(label_match)) == label_match then
			if user then
				if #recipient < #label then
					recipient = label;
				end
			else
				recipient = label;
			end
		end
	end
	
	-- If no user, then bail out
	if not recipient then
		if User.isHost() then
			SystemMessage("Whisper recipient not found \rUsage: /w [recipient] [message]");
		else
            processWhisperGM(params);
		end
		return;
	end
	
	-- Get the message text
	local msgtext = string.sub(params, string.len(recipient) + 1);
	
	-- If GM then process directly
	sendSpecialMessage(SPECIAL_MSGTYPE_WHISPER, {recipient, msgtext});
end

function processWhisperGM(params)
	if not User.isHost() then
		sendSpecialMessage(SPECIAL_MSGTYPE_WHISPER, {"", params});
	end
end

function handleWhisper(msguser, msgidentity, paramlist)
	-- Get the parameters
	local recipient = paramlist[1];
	local whispertext = paramlist[2];
	
	-- Build the message to deliver
	local msg = {font = "whisperfont"};
	msg.text = whispertext;

	-- Cycle through identities to determine user for recipient
	local recipient_user = "";
	for k, v in ipairs(User.getAllActiveIdentities()) do
		local label = User.getIdentityLabel(v);
		if label == recipient then
			recipient_user = User.getIdentityOwner(v);
		end
	end
	
	-- Deliver message to recipient
	if msgidentity == "" then
		msg.sender = "[w] GM";
	else
		msg.sender = "[w] " .. msgidentity;
	end
	if recipient == "" then
		addMessage(msg);
	else
		if recipient_user ~= "" then
			deliverMessage(msg, recipient_user);
		else
			SystemMessage("[ERROR] Unable to resolve owner of identity = " .. recipient);
		end
	end

	-- Also, deliver confirmation to the sender
	if recipient == "" then
		msg.sender = "[w] -> GM";
	else
		msg.sender = "[w] -> " .. recipient;
	end
	if msgidentity == "" then
		addMessage(msg);
	else
		deliverMessage(msg, msguser);
	end
	
	-- Finally, if the GM was not sender or recipient, then see if the show player messages option is on
	if OptionsManager.isOption("SHPW", "on") and msgidentity ~= "" and recipient ~= "" then
		msg.sender = "[w] " .. msgidentity .. " -> " .. recipient;
		addMessage(msg);
	end
end


--
--
--  DICE TOWER
--
--

function handleDiceTower(msguser, msgidentity, paramlist)
	-- Get the parameters
	local droptype = paramlist[1];
	local desc = paramlist[2];
	local dicestr = paramlist[3];
	
	-- Build the description string
	local roll_desc = "[TOWER] ";
	if msgidentity ~= "" then
		roll_desc = roll_desc .. msgidentity .. " -> ";
	else
		roll_desc = roll_desc .. "GM -> ";
	end
	roll_desc = roll_desc .. desc;

	-- Roll the dice
	local dice, modifier = StringManager.convertStringToDice(dicestr);
	RulesManager.dclkAction(droptype, modifier, roll_desc, nil, nil, dice, true);
	
	-- Return a confirmation to client, if needed
	if msguser ~= "" then
		-- Build the message
		local clientmsg = {font = "chatfont", icon = "dicetower_icon", sender = "[TOWER]", text = ""};
		if desc ~= "" then
			clientmsg.text = desc .. ": ";
		end
		clientmsg.text = clientmsg.text .. dicestr;
		
		-- Deliver the message
		deliverMessage(clientmsg, msguser);
	end
end


--
--
--  APPLY DAMAGE (CT FIELDS)
--
--

function handleApplyDamage(msguser, msgidentity, paramlist)
	-- Get the parameters
	local total = tonumber(paramlist[1]) or 0;
	local dmgstr = paramlist[2];
	local sTargetType = paramlist[3];
	local sTargetCreatureNode = paramlist[4];
	local sTargetCTNode = paramlist[5];
	local sSourceCTNode = paramlist[6];
	
	-- GET THE TARGET ACTOR
	local rTargetActor = CombatCommon.getActor("ct", sTargetCTNode);
	if not rTargetActor then
		rTargetActor = CombatCommon.getActor(sTargetType, sTargetCreatureNode);
	end
	
	-- GET THE SOURCE ACTOR
	local rSourceActor = CombatCommon.getActor("ct", sSourceCTNode);
	
	-- Apply the damage
	RulesManager.applyDamage(rTargetActor, total, dmgstr, rSourceActor);
end


--
--
--  APPLY EFFECT (CT FIELDS)
--
--

function handleApplyEffect(msguser, msgidentity, paramlist)
	-- Get the parameters
	local sCTEntryNode = paramlist[1];

	local rEffect = {};
	rEffect.sName = paramlist[2];
	rEffect.sExpire = paramlist[3];
	rEffect.nSaveMod = tonumber(paramlist[4]) or 0;
	rEffect.nInit = tonumber(paramlist[5]) or 0;
	rEffect.sSource = paramlist[6];
	rEffect.nGMOnly = tonumber(paramlist[7]) or 0;
	rEffect.sApply = paramlist[8];
	
	local sEffectTargetNode = paramlist[9] or "";
	
	-- Get the combat tracker node
	local nodeCTEntry = DB.findNode(sCTEntryNode);
	if not nodeCTEntry then
		SystemMessage("[ERROR] Unable to resolve CT effect application on node = " .. sCTEntryNode);
		return;
	end
	
	-- Apply the damage
	EffectsManager.addEffect(msguser, msgidentity, nodeCTEntry, rEffect, sEffectTargetNode);
end

--
--
--  EXPIRE EFFECT
--
--

function handleExpireEffect(msguser, msgidentity, paramlist)
	-- Get the combat tracker node
	local nodeActor = DB.findNode(paramlist[1]);
	if not nodeActor then
		SystemMessage("[ERROR] Unable to find actor to remove effect from = " .. paramlist[1]);
		return;
	end
	
	-- Get the combat tracker node
	local nodeEffect = DB.findNode(paramlist[2]);
	if not nodeEffect then
		SystemMessage("[ERROR] Unable to find effect to remove = " .. paramlist[2]);
		return;
	end
	
	-- Get the parameters
	local nExpireType = tonumber(paramlist[3]) or 0;
	
	-- Apply the damage
	EffectsManager.expireEffect(nodeActor, nodeEffect, nExpireType);
end

--
--
--  FAILED SAVE EFFECT
--
--

function handleFailedSaveEffect(msguser, msgidentity, paramlist)
	-- Get the combat tracker node
	local nodeActor = DB.findNode(paramlist[1]);
	if not nodeActor then
		SystemMessage("[ERROR] Unable to find actor to remove effect from = " .. paramlist[1]);
		return;
	end
	
	-- Get the combat tracker node
	local nodeEffect = DB.findNode(paramlist[2]);
	if not nodeEffect then
		SystemMessage("[ERROR] Unable to find effect to remove = " .. paramlist[2]);
		return;
	end
	
	-- Apply the damage
	EffectsManager.handleFailedSave(nodeActor, nodeEffect);
end

--
--
--  END TURN (CT)
--
--

function handleEndTurn(msguser, msgidentity, paramlist)
	CombatCommon.endTurn(msguser);
end

--
--
--  REMOVE CLIENT TARGET
--
--

function handleRemoveClientTarget(msguser, msgidentity, paramlist)
	local sSourceName = paramlist[1];
	local sTargetNode = paramlist[2];
	TargetingManager.removeClientTarget(msguser, sSourceName, sTargetNode);
end